package com.zlyx.easy.swagger.plugins;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static com.zlyx.easy.swagger.utils.Annotations.resolvedTypeFromOperation;
import static com.zlyx.easy.swagger.utils.Annotations.resolvedTypeFromResponse;
import static com.zlyx.easy.swagger.utils.ResponseHeaders.headers;
import static com.zlyx.easy.swagger.utils.ResponseHeaders.responseHeaders;
import static springfox.documentation.schema.ResolvedTypes.modelRefFactory;
import static springfox.documentation.spi.schema.contexts.ModelContext.returnValue;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import com.zlyx.easy.core.collections.Lists;
import com.zlyx.easy.swagger.annotations.SpringMapping;
import com.zlyx.easy.swagger.config.EasySwaggerConfiguration;

import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ResponseHeader;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelReference;
import springfox.documentation.schema.TypeNameExtractor;
import springfox.documentation.service.Header;
import springfox.documentation.service.Operation;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.contexts.ModelContext;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)
public class EasySwaggerResponseMessageReader implements OperationBuilderPlugin {

	private final TypeNameExtractor typeNameExtractor;
	private final TypeResolver typeResolver;

	@Autowired
	public EasySwaggerResponseMessageReader(TypeNameExtractor typeNameExtractor, TypeResolver typeResolver) {
		this.typeNameExtractor = typeNameExtractor;
		this.typeResolver = typeResolver;
	}

	@Override
	public void apply(OperationContext context) {
		context.operationBuilder().responseMessages(read(context));
	}

	@Override
	public boolean supports(DocumentationType delimiter) {
		return SwaggerPluginSupport.pluginDoesApply(delimiter);
	}

	protected Set<ResponseMessage> read(OperationContext context) {
		Set<ResponseMessage> responseMessages = newHashSet();
		Optional<SpringMapping> annotation = context.findAnnotation(SpringMapping.class);
		if(annotation.isPresent() && !annotation.get().hidden()) {
			Operation operation = context.operationBuilder().build();
			ResolvedType defaultResponse = context.getReturnType();
			Optional<ResolvedType> operationResponse = annotation.transform(resolvedTypeFromOperation(typeResolver, defaultResponse));
			Optional<ResponseHeader[]> defaultResponseHeaders = annotation.transform(responseHeaders());
			Map<String, Header> defaultHeaders = newHashMap();
			if (defaultResponseHeaders.isPresent()) {
				defaultHeaders.putAll(headers(defaultResponseHeaders.get()));
			}
			Set<Integer> codes = getResponseCode(operation);
			int[] responseCodes = annotation.get().codes();
			com.zlyx.easy.swagger.model.ResponseMessage message = null;
			for(int code : responseCodes) {
				message = EasySwaggerConfiguration.getResponseMessage(code);
				if (!codes.contains(code) && message != null) {
					codes.add(code);
					ModelContext modelContext = returnValue(context.getGroupName(), message.getResponseClass(),
							context.getDocumentationType(), context.getAlternateTypeProvider(),
							context.getGenericsNamingStrategy(), context.getIgnorableParameterTypes());
					Optional<ModelReference> responseModel = Optional.absent();
					ResolvedType type = operationResponse.get();
					if(message.getResponseClass() != null) {
						type = typeResolver.resolve(message.getResponseClass());
					}
					responseModel = Optional.of(modelRefFactory(modelContext, typeNameExtractor).apply(context.alternateFor(type)));
					responseMessages.add(new ResponseMessageBuilder()
							.code(code)
							.message(message.getMessage())
							.responseModel(responseModel.orNull())
							.headersWithDescription(message.getHeaders()).build());
				}
			}
			ApiResponse[] apiResponseAnnotations = annotation.get().apiResponses();
			
			for (ApiResponse apiResponse : apiResponseAnnotations) {
				handleResponse(context, operationResponse, defaultHeaders, responseMessages, codes, apiResponse);
			}
			Optional<ApiResponse> response = context.findAnnotation(ApiResponse.class);
			if (response.isPresent()) {
				handleResponse(context, operationResponse, defaultHeaders, responseMessages, codes, response.get());
			}
		}
		return responseMessages;
	}

	private void handleResponse(OperationContext context, Optional<ResolvedType> operationResponse,
			Map<String, Header> defaultHeaders, Set<ResponseMessage> responseMessages, Set<Integer> codes,
			ApiResponse apiResponse) {
		if (!codes.contains(apiResponse.code())) {
			codes.add(apiResponse.code());
			ModelContext modelContext = returnValue(context.getGroupName(), apiResponse.response(),
					context.getDocumentationType(), context.getAlternateTypeProvider(),
					context.getGenericsNamingStrategy(), context.getIgnorableParameterTypes());
			Optional<ModelReference> responseModel = Optional.absent();
			Optional<ResolvedType> type = resolvedType(null, apiResponse);
			if (isSuccessful(apiResponse.code())) {
				type = type.or(operationResponse);
			}
			if (type.isPresent()) {
				responseModel = Optional
						.of(modelRefFactory(modelContext, typeNameExtractor).apply(context.alternateFor(type.get())));
			}
			Map<String, Header> headers = newHashMap(defaultHeaders);
			headers.putAll(headers(apiResponse.responseHeaders()));
			responseMessages.add(new ResponseMessageBuilder()
					.code(apiResponse.code())
					.message(apiResponse.message())
					.responseModel(responseModel.orNull())
					.headersWithDescription(headers).build());
		}
	}

	private Set<Integer> getResponseCode(Operation operation) {
		Set<Integer> codes = Lists.newHashSet();
		Set<ResponseMessage> messages = operation.getResponseMessages();
		Iterator<ResponseMessage> its = messages.iterator();
		while (its.hasNext()) {
			codes.add(its.next().getCode());
		}
		return codes;
	}

	static boolean isSuccessful(int code) {
		try {
			return HttpStatus.Series.SUCCESSFUL.equals(HttpStatus.Series.valueOf(code));
		} catch (Exception ignored) {
			return false;
		}
	}

	private Optional<ResolvedType> resolvedType(ResolvedType resolvedType, ApiResponse apiResponse) {
		return fromNullable(resolvedTypeFromResponse(typeResolver, resolvedType).apply(apiResponse));
	}

}
